/*
 * @(#)BeanTableModel.java   13/08/20
 * 
 * Copyright (c) 2013 DieHard Development
 *
 * All rights reserved.
   Released under the BSD 3 clause license
Redistribution and use in source and binary forms, with or without modification, 
are permitted provided that the following conditions are met:

    Redistributions of source code must retain the above copyright notice, this 
    list of conditions and the following disclaimer. Redistributions in binary 
    form must reproduce the above copyright notice, this list of conditions and 
    the following disclaimer in the documentation and/or other materials 
    provided with the distribution. Neither the name of the DieHard Development 
    nor the names of its contributors may be used to endorse or promote products 
    derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 *
 *
 *
 */


package showtest;

//~--- JDK imports ------------------------------------------------------------

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

/**
 *  The BeanTableModel will use reflection to determine the columns of
 *  data to be displayed in the table model. Reflection is used to find all
 *  the methods declared in the specified bean. The criteria used for
 *  adding columns to the model are:
 *
 *  a) the method name must start with either "get" or "is"
 *  b) the parameter list for the method must contain 0 parameters
 *
 *  You can also specify an ancestor class in which case the declared methods
 *  of the ancestor and all its descendents will be included in the model.
 *
 *  A column name will be assigned to each column based on the method name.
 *
 *  The cell will be considered editable when a corresponding "set" method
 *  name is found.
 *
 *  Reflection will also be used to implement the getValueAt() and
 *  setValueAt() methods.
 *
 * @param <T>
 */
public class BeanTableModel<T> extends RowTableModel<T> {

    /**
     *
     */
    private static final long serialVersionUID = 1L;

    // Map "type" to "class". Class is needed for the getColumnClass() method.
    @SuppressWarnings("rawtypes")
    private static Map<Class, Class> primitives = new HashMap<Class, Class>(10);
    private static final Logger LOG = Logger.getLogger(BeanTableModel.class.getName());

    static {
        primitives.put(Boolean.TYPE, Boolean.class);
        primitives.put(Byte.TYPE, Byte.class);
        primitives.put(Character.TYPE, Character.class);
        primitives.put(Double.TYPE, Double.class);
        primitives.put(Float.TYPE, Float.class);
        primitives.put(Integer.TYPE, Integer.class);
        primitives.put(Long.TYPE, Long.class);
        primitives.put(Short.TYPE, Short.class);
    }

    private final List<ColumnInformation> columns = new ArrayList<ColumnInformation>();
    private Class<?> beanClass;
    private Class<?> ancestorClass;

    /**
     *  Constructs an empty <code>BeanTableModel</code> for the specified bean.
     *
     *  @param beanClass  class of the beans that will be added to the model.
     *                    The class is also used to determine the columns that
     *                    will be displayed in the model
     */
    public BeanTableModel(Class<?> beanClass) {
        this(beanClass, beanClass, new ArrayList<T>());
    }

    /**
     *  Constructs an empty <code>BeanTableModel</code> for the specified bean.
     *
     *  @param beanClass      class of the beans that will be added to the model.
     *  @param ancestorClass  the methods of this class and its descendents down
     *                                               to the bean class can be included in the model.
     */
    public BeanTableModel(Class<?> beanClass, Class<?> ancestorClass) {
        this(beanClass, ancestorClass, new ArrayList<T>());
    }

    /**
     *  Constructs an empty <code>BeanTableModel</code> for the specified bean.
     *
     *  @param beanClass      class of the beans that will be added to the model.
     *  @param modelData      the data of the table
     */
    public BeanTableModel(Class<?> beanClass, List<T> modelData) {
        this(beanClass, beanClass, modelData);
    }

    /**
     *  Constructs an empty <code>BeanTableModel</code> for the specified bean.
     *
     *  @param beanClass      class of the beans that will be added to the model.
     *  @param ancestorClass  the methods of this class and its descendents down
     *                                               to the bean class can be included in the model.
     *  @param modelData      the data of the table
     */
    public BeanTableModel(Class<?> beanClass, Class<?> ancestorClass, List<T> modelData) {
        super(beanClass);
        this.beanClass = beanClass;
        this.ancestorClass = ancestorClass;

        // Use reflection on the beanClass and ancestorClass to find properties
        // to add to the TableModel
        createColumnInformation();

        // Initialize the column name List to the proper size. The actual
        // column names will be reset in the resetModelDefaults() method.
        List<String> columnNames = new ArrayList<String>();

        for (ColumnInformation info : columns) {
            columnNames.add(info.getName());
        }

        // Reset all the values in the RowTableModel
        super.setDataAndColumnNames(modelData, columnNames);
        resetModelDefaults();
    }

    /*
     *  Use reflection to find all the methods that should be included in the
     *  model.
     */
    private void createColumnInformation() {
        Method[] theMethods = beanClass.getMethods();

        // Check each method to make sure it should be used in the model
        for (Method theMethod : theMethods) {
            if ((theMethod.getParameterTypes().length == 0)
                    && ancestorClass.isAssignableFrom(theMethod.getDeclaringClass())) {
                String methodName = theMethod.getName();

                if (theMethod.getName().startsWith("get")) {
                    buildColumnInformation(theMethod, methodName.substring(3));
                }

                if (theMethod.getName().startsWith("is")) {
                    buildColumnInformation(theMethod, methodName.substring(2));
                }
            }
        }
    }

    /*
     *      We found a method candidate so gather the information needed to fully
     *  implemennt the table model.
     */
    private void buildColumnInformation(Method theMethod, String theMethodName) {

        // Make sure the method returns an appropriate type
        Class<?> returnType = getReturnType(theMethod);

        if (returnType == null) {
            return;
        }

        // Convert the method name to a display name for each column and
        // then check for a related "set" method.
        String headerName = formatColumnName(theMethodName);
        Method setMethod = null;

        try {
            String setMethodName = "set" + theMethodName;

            setMethod = beanClass.getMethod(setMethodName, theMethod.getReturnType());
        } catch (NoSuchMethodException e) {
            LOG.log(java.util.logging.Level.SEVERE, null, e);
        }

        // We have all the information we need, so save it for later use
        // by the table model methods.
        ColumnInformation ci = new ColumnInformation(headerName, returnType, theMethod, setMethod);

        columns.add(ci);
    }

    /*
     *  Make sure the return type of the method is something we can use
     */
    private Class<?> getReturnType(Method theMethod) {
        Class<?> returnType = theMethod.getReturnType();

        if (returnType.isInterface() || returnType.isArray()) {
            return null;
        }

        // The primitive class type is different then the wrapper class of the
        // primitive. We need the wrapper class.
        if (returnType.isPrimitive()) {
            returnType = primitives.get(returnType);
        }

        return returnType;
    }

    /*
     *  Use information collected from the bean to set model default values.
     */
    private void resetModelDefaults() {
        columnNames.clear();

        for (int i = 0; i < columns.size(); i++) {
            ColumnInformation info = columns.get(i);

            columnNames.add(info.getName());
            super.setColumnClass(i, info.getReturnType());
            super.setColumnEditable(i, (info.getSetter() == null)
                                       ? false
                                       : true);
        }
    }

    /**
     *  Returns an attribute value for the cell at <code>row</code>
     *  and <code>column</code>.
     *
     *  @param   row        the row whose value is to be queried
     *  @param   column     the column whose value is to be queried
     *  @return             the value Object at the specified cell
     */
    @Override
    public Object getValueAt(int row, int column) {
        ColumnInformation ci = columns.get(column);
        Object value = null;

        try {
            value = ci.getGetter().invoke(getRow(row));
        } catch (IllegalAccessException | InvocationTargetException e) {
            LOG.log(java.util.logging.Level.SEVERE, null, e);
        }

        return value;
    }

    /**
     *  Sets the object value for the cell at <code>column</code> and
     *  <code>row</code>.  <code>value</code> is the new value.  This method
     *  will generate a <code>tableChanged</code> notification.
     *
     * @param   value           the new value; this can be null
     * @param   row             the row whose value is to be changed
     * @param   column          the column whose value is to be changed
     */
    @Override
    public void setValueAt(Object value, int row, int column) {
        ColumnInformation ci = columns.get(column);

        try {
            Method setMethod = ci.getSetter();

            if (setMethod != null) {
                setMethod.invoke(getRow(row), value);
                fireTableCellUpdated(row, column);
            }
        } catch (IllegalAccessException | InvocationTargetException e) {
            LOG.log(java.util.logging.Level.SEVERE, null, e);
        }
    }

    /**
     *  You are not allowed to change the class of any column.
     *
     * @param column
     * @param columnClass
     */
    @Override
    public void setColumnClass(int column, Class<?> columnClass) {}

    /**
     *  Sets the editability for the specified column.
     *
     *      Override to make sure you can't set a column editable that doesn't
     *  have a defined setter method.
     *
     *  @param  column       the column whose Class is being changed
     *  @param  isEditable   indicates if the column is editable or not
     */
    @Override
    public void setColumnEditable(int column, boolean isEditable) {
        ColumnInformation ci = columns.get(column);

        if (isEditable && (ci.getSetter() == null)) {
            return;
        }

        super.setColumnEditable(column, isEditable);
    }

    /**
     *  Convenience method to change the generated column header name.
     *
     *  This method must be invoked before the model is added to the table.
     *
     *  @param   column     the column whose value is to be queried
     * @param name
     */
    public void setColumnName(int column, String name) {
        ColumnInformation ci = columns.get(column);

        ci.setName(name);
        resetModelDefaults();
    }

    /*
     *  Columns are created in the order in which they are defined in the
     *  bean class. This method will sort the columns by colum header name.
     *
     *  This method must be invoked before the model is added to the table.
     */

    /**
     * Method description
     *
     */
    public void sortColumnNames() {
        Collections.sort(columns);
        resetModelDefaults();
    }

    /*
     *  Class to hold data required to implement the TableModel interface
     */
    private class ColumnInformation implements Comparable<ColumnInformation> {
        private String name;
        private final Class<?> returnType;
        private final Method getter;
        private final Method setter;

        /**
         * Constructs ...
         *
         *
         * @param name
         * @param returnType
         * @param getter
         * @param setter
         */
        ColumnInformation(String name, Class<?> returnType, Method getter, Method setter) {
            this.name = name;
            this.returnType = returnType;
            this.getter = getter;
            this.setter = setter;
        }

        /*
         *  The column class of the model
         */

        /**
         * Method description
         *
         *
         * @return
         */
        public Class<?> getReturnType() {
            return returnType;
        }

        /*
         *  Used by the getValueAt() method to get the data for the cell
         */

        /**
         * Method description
         *
         *
         * @return
         */
        public Method getGetter() {
            return getter;
        }

        /*
         *  The value used as the column header name
         */

        /**
         * Method description
         *
         *
         * @return
         */
        public String getName() {
            return name;
        }

        /*
         *  Used by the setValueAt() method to update the bean
         */

        /**
         * Method description
         *
         *
         * @return
         */
        public Method getSetter() {
            return setter;
        }

        /*
         *  Use to change the column header name
         */

        /**
         * Method description
         *
         *
         * @param name
         */
        public void setName(String name) {
            this.name = name;
        }

        /*
         *  Implement the natural sort order for this class
         */

        /**
         * Method description
         *
         *
         * @param o
         *
         * @return
         */
        @Override
        public int compareTo(ColumnInformation o) {
            return getName().compareTo(o.getName());
        }
    }
}


//~ Formatted in DD Std on 13/08/20
